/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.components;

import com.chasekernan.hxnova.core.components.ComponentSet;
import com.chasekernan.hxnova.core.races.LRTSet;
import com.chasekernan.hxnova.core.races.PRTSet;
import com.chasekernan.hxnova.core.races.Race;
import com.chasekernan.hxnova.utils.XmlSerializer;
import haxe.xml.Check;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.races.TechLevel;
import Type;

/**
    The settings for a components availability to a specific lrt.
**/
enum LRTSetting {
    Any;
    Not(lrts : Array<LRT>);
    Only(lrts : Array<LRT>);
}

/**
    The base class for a ship/starbase components that contains most methods
    so that individual component types have to specify little besides their
    fields.
**/
class Component {
    
    public var name : String;
    public var mass : Int;
    public var description : String;
    public var picture : String;
    
    public var mineralCost : MineralSet;
    public var resourceCost : Int;
    public var requiredTech : TechLevel;
    
    public var availableToPRTs : Array<PRT>;
    public var lrts : LRTSetting;
    
    /**
        Creates a new component with everything defaulted or zeroed.
    **/
    public function new(name : String) {
        this.name = name;
        mass = 0;
        description = "";
        picture = "";
        mineralCost = new MineralSet();
        resourceCost = 0;
        requiredTech = new TechLevel(0);
        
        availableToPRTs = new Array<PRT>();
        for(i in PRTSet.PRT_LIST) availableToPRTs.push(i);
        
        lrts = Any;
    }
    
    public function getXmlTitle() : String {
        return ComponentSet.getXmlName(Type.getClass(this));
    }
    
    public function getRules() : Rule {
        return RNode(getXmlTitle(), getAtt(this), RList(getChildren(), false));
    }
    
    public function isAvailableTo(race : Race) : Bool {
        var flag = true;
        for(i in availableToPRTs) if(race.prt == i){ flag = false; break;}
        if(flag) return false;
        
        switch(lrts) {
            case Any:
                //do nothing
            case Not(notLRTs):
                for(i in race.lrts) {for(j in notLRTs) {return i != j;}}
            case Only(onlyLRTs):
                for(i in race.lrts) {
                    var flag = true;
                    for(j in onlyLRTs) {if(j == i) {flag = false; break;}}
                    if(flag) return false;
                }
        }
        
        return true;
    }
    
    public function isAvailableToTech(level : TechLevel) : Bool {
        return level.isGreaterThanOrEqualTo(requiredTech);
    }
    
    /**
        Adds the prt to the prts that this component is available to.
        If its already on the list, nothing happens.
    **/
    public function addPRT(prt : PRT) {
        for(i in availableToPRTs) if(prt == i){ return;}
        availableToPRTs.push(prt);
    }
    
    /**
        Makes this component unavailable to a prt.
        Nothing happens if the prt is not in the list.
    **/
    public function removePRT(prt : PRT) {
        for(i in 0...availableToPRTs.length) {
            if(availableToPRTs[i] == prt) availableToPRTs.splice(i, 1);
        }
    }
    
    public function setLRTs(lrts : LRTSetting) {
        //remove any lrts that are specified twice
        switch(lrts) {
            case Any:
                this.lrts = lrts;
            case Not(notLRTs):
                this.lrts = Not(removeDuplicates(notLRTs));
            case Only(onlyLRTs):
                this.lrts = Only(removeDuplicates(onlyLRTs));
        }
    }
    
    private function removeDuplicates<T>(a : Array<T>) : Array<T> {
        var newA : Array<T> = new Array();
        
        for(i in 0...a.length) {
            var flag = true;
            for(j in newA) {if(j == a[i]) {flag = false; break;}}
            if(flag) newA.push(a[i]);
        }
        return newA;
    }
    
    //returns the xml attributes of a component, where o is an instance of a
    //component.
    private function getAtt(?o : Dynamic) : Array<Attrib> {
        var att = [Att("name"), Att("mass", FInt), Att('available_to_prts'), 
                Att('lrts'), Att('picture')];
        
        if(o == null) return att;
        else {
            var fields = getFields(o);
            var underscored = getUnderscoreVersion(fields);
            
            for(i in 0...fields.length) {
                var type = Type.typeof(Reflect.field(o, fields[i]));
                
                if(type == TBool) att.push(Att(underscored[i], FBool));
                else if(type == TInt) att.push(Att(underscored[i], FInt));
                else att.push(Att(underscored[i]));
            }
        }
        return att;
    }
    
    //returns the fields of an instance that are not in the component class.
    private function getFields(o : Dynamic) : Array<String> {
        var comStrings = Type.getInstanceFields(Component);
        var otherStrings = Type.getInstanceFields(Type.getClass(o));
        var newStrings = new Array<String>();
            
        for(i in otherStrings) {
            var contains = false;
            for(j in comStrings) {if(j == i) {contains = true; break;}}
            if(contains) continue;
            else {
                if (switch(Type.typeof(Reflect.field(o, i))) {
                    case TInt: true;
                    
                    case TFloat: true;
                    
                    case TBool: true;
                    
                    default: false;
                    
                }) newStrings.push(i);
            }
        }
        
        return newStrings;
    }
    
    /**
        Returns the the_underscore_version of a camelCaseWord.
    **/
    public static function getUnderscoreVersion(a : Array<String>) : 
            Array<String> {
        var newStrings = new Array<String>();
        for(i in a) {
            //just lowercase the first letter and skip it
            i = i.substr(0, 1).toLowerCase() + i.substr(1);
            for(j in 1...i.length) {
                if(i.charAt(j).toUpperCase() == i.charAt(j)) {
                    i  = i.substr(0, j) + "_" + i.charAt(j).toLowerCase() + 
                            i.substr(j + 1);
                }
            }
            newStrings.push(i);
        }
        
        return newStrings;
    }
    
    private function getChildren() : Array<Rule> {
        return [RNode("description"), RNode('costs', [], RList([
                    MineralSet.getXmlRules(), 
                    RNode("resource_cost", [], RData(FInt)), 
                    TechLevel.getXmlRules()], 
                false))];
    }
    
    private function setAttribsAndChildren(xml : Xml, ?o : Dynamic) {
        xml.set('name', name);
        xml.set('mass', Std.string(mass));
        xml.set('picture', picture);
        xml.set('available_to_prts', availableToPRTs.join(', '));
        switch(lrts) {
            case Any:
                xml.set('lrts', 'any');
            case Not(notLRTs):
                xml.set('lrts', 'not '+notLRTs.join(', '));
            case Only(onlyLRTs):
                xml.set('lrts', 'only '+onlyLRTs.join(', '));
        }
        
        if(o != null) {
            var fields = getFields(o);
            var underscored = getUnderscoreVersion(fields);
            
            for(i in 0...fields.length) 
                xml.set(underscored[i], Std.string(
                        Reflect.field(o, fields[i])));
            
        }
        
        xml.addChild(Xml.parse('<description><![CDATA['+description+']]></description>')
                .firstElement());
        
        var costs = Xml.parse('<costs></costs>').firstElement();
        costs.addChild(MineralSet.writeToXml(mineralCost));
        costs.addChild(Xml.parse('<resource_cost>'+Std.string(resourceCost) + 
                '</resource_cost>').firstElement());
        costs.addChild(TechLevel.writeToXml(requiredTech));
        xml.addChild(costs);
    }
    
    /**
        Writes a component to an xml file.
        This is rather complicated as this uses reflection to determine the
        fields of a component subsclass and creates xml elements/attributes for 
        them.
    **/
    public static function writeToXml(c : Component) : Xml {
        var xml = Xml.parse('<' + c.getXmlTitle()+'></' + c.getXmlTitle()+'>')
                .firstChild();
        
        c.setAttribsAndChildren(xml, c);
        
        //Check.checkNode(xml, c.getRules());
        
        return xml;
    }
    
    //again, this uses reflection to set all the fields of a component using the
    //xml data given.
    //Only public so that base classes can access it in static methods.
    public function generateFromComponentXml(xml : Xml, ?o : Dynamic) {
        try {
            name = xml.get('name');
            picture = xml.get('picture');
            mass = Std.parseInt(xml.get('mass'));
            description = xml.elementsNamed('description').next().firstChild()
                    .nodeValue;
            
            availableToPRTs = new Array<PRT>();
            var tempA : Array<String> = 
                    xml.get('available_to_prts').split(', ');
            if (tempA.length != 0 && tempA[0] != " " && tempA[0] != "") {
                for (i in tempA) availableToPRTs.push(PRTSet.getPRTByName(i));
            }
            
            var s : String = xml.get('lrts');
            
            var anyEReg : EReg = ~/any/i;
            var notEReg : EReg = ~/not[ ]+/i;
            var onlyEReg : EReg = ~/only[ ]+/i;
            
            if(anyEReg.match(s)) lrts = Any;
            else if(notEReg.match(s)) {
                var tempArray : Array<String> = 
                        notEReg.split(s).pop().split(', ');
                var lrtArray : Array<LRT> = new Array();
                for(i in tempArray) lrtArray.push(LRTSet.getLRTByName(i));
                lrts = Not(lrtArray);
            }
            else if(onlyEReg.match(s)) {
                var tempArray : Array<String> = 
                        onlyEReg.split(s).pop().split(', ');
                var lrtArray : Array<LRT> = new Array();
                for(i in tempArray) lrtArray.push(LRTSet.getLRTByName(i));
                lrts = Only(lrtArray);
            } 
            else throw "LRT restrictions do not conform.";

            
            resourceCost = Std.parseInt(xml.elementsNamed('costs').next()
                    .elementsNamed('resource_cost').next().firstChild()
                    .nodeValue);
            mineralCost = MineralSet.readFromXml(xml.elementsNamed('costs')
                    .next().elementsNamed('mineral_set').next());
            requiredTech = TechLevel.readFromXml(xml.elementsNamed('costs')
                    .next().elementsNamed('tech_level').next());
            
            if(o != null) {
                var fields = getFields(o);
                var underscored = getUnderscoreVersion(fields);
            
                for (i in 0...fields.length) {
                    var type = Type.typeof(Reflect.field(o, fields[i]));
                    
                    switch(type) {
                        
                        case TBool:
                            Reflect.setField(o, fields[i], 
                                xml.get(underscored[i]) == "true");
                                
                        case TInt:
                            Reflect.setField(o, fields[i],
                                Std.parseInt(xml.get(underscored[i])));
                                
                        case TFloat:
                            Reflect.setField(o, fields[i],
                                Std.parseFloat(xml.get(underscored[i])));
                                
                        default:
                            //ignore
                    }
                }
            }
        } catch (e : Dynamic) {
            throw "Problem loading xml file for component: "+ e;
        }
    }
    
    /**
        Reads from a component xml file.
        Note: this function is only for components that don't fit into 
        individual classes (very rare, maybe MT parts?).
    **/
    public static function readFromXml(xml : Xml) : Component {
        try {
            var c = new Component(" ");
            Check.checkNode(xml, c.getRules());
            c.generateFromComponentXml(xml);
            
            return c;
        } catch (e : Dynamic) {
            throw "Problem loading xml file for component: "+ e;
        }
    }
}